/*
 * Copyright (c) 2012 Andrea De Rinaldis <ozioso at ipignoli.dyndns.org>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.dyndns.ipignoli.droidcruise.routes;

import java.util.Iterator;
import java.util.List;
import org.dyndns.ipignoli.droidcruise.DroidCruise;
import android.location.Location;
import android.util.Log;


public class Route{
  private static final int     ROAD_WIDTH           = 16;
  private static final int    RADAR_ALERT_DISTANCE = 1000;
  private static final double M_PER_LAT_DEG        = (110574 + 111699) / 2;
  private static final double M_PER_LON_DEG        = 11319;

  private static class LastFound{
    private Location a;
    private double   distanceFromA;

    private LastFound(Location a, Location l){
      this.a = a;
      distanceFromA = a.distanceTo(l);
    }

    private boolean getDirection(Location l){
      return a.distanceTo(l) > distanceFromA;
    }
  }

  private String         name;
  private int            limit;
  private List<Location> locations;
  private List<Location> radars;
  private LastFound      lastFound;
  private double         maxLatitude, maxLongitude, minLatitude, minLongitude;
  private double         length;
  private double         distance;

  public Route(String name, int limit, List<Location> locations,
      List<Location> radars){
    this.name = name;
    this.limit = limit;
    this.locations = locations;
    this.radars = radars;
    lastFound = null;
    maxLatitude = -91d;
    maxLongitude = -181d;
    minLatitude = 91d;
    minLongitude = 181d;
    length = 0l;
    distance = 0d;
  }

  public int getLimit(){
    return limit;
  }

  public boolean addLocation(Location l){
    if(l.getLatitude() > maxLatitude)
      maxLatitude = l.getLatitude();
    if(l.getLatitude() < minLatitude)
      minLatitude = l.getLatitude();
    if(l.getLongitude() > maxLongitude)
      maxLongitude = l.getLongitude();
    if(l.getLongitude() < minLongitude)
      minLongitude = l.getLongitude();
    if(locations.size() > 0)
      length += l.distanceTo(locations.get(locations.size() - 1));
    return locations.add(l);
  }

  public boolean addRadar(Location l){
    return radars.add(l);
  }

  public double getLength(){
    return length;
  }

  private void resetDistance(){
    distance = 0;
  }

  private void incrementDistance(double d){
    distance += d;
  }

  public synchronized double getDistance(){
    return distance;
  }

  public Iterator<Location> locationIterator(){
    return locations.iterator();
  }

  public Iterator<Location> radarIterator(){
    return radars.iterator();
  }

  public boolean isRadarApproaching(Location l){
    if(radars.size() == 0 || locations.size() == 0)
      return false;

    Location first = locations.get(0);
    for(Location radar: radars){
      if(l.distanceTo(radar) < RADAR_ALERT_DISTANCE
          && first.distanceTo(l) < first.distanceTo(radar))
        return true;
    }
    return false;
  }

  public synchronized boolean isInRoute(Location l, int gpsAccuracy){
    if(locations.size() == 0)
      return false;

    double minLatitudeRef =
        minLatitude - (gpsAccuracy + ROAD_WIDTH) / M_PER_LAT_DEG;
    double maxLatitudeRef =
        maxLatitude + (gpsAccuracy + ROAD_WIDTH) / M_PER_LAT_DEG;
    double minLongitudeRef =
        minLongitude - (gpsAccuracy + ROAD_WIDTH)
            / (M_PER_LON_DEG * Math.cos(minLatitude));
    double maxLongitudeRef =
        maxLongitude + (gpsAccuracy + ROAD_WIDTH)
            / (M_PER_LON_DEG * Math.cos(maxLatitude));

    if(l.getLatitude() < minLatitudeRef || l.getLatitude() > maxLatitudeRef
        || l.getLongitude() < minLongitudeRef
        || l.getLongitude() > maxLongitudeRef)
      return false;

    resetDistance();
    double accuracy = Math.sqrt(Math.pow(gpsAccuracy, 2) * 2) + ROAD_WIDTH;
    Location last = null;
    for(int i = 1; i < locations.size(); i++){
      last = locations.get(i - 1);
      double ab = last.distanceTo(locations.get(i));
      double ac = last.distanceTo(l);
      double bc = locations.get(i).distanceTo(l);
      if(Math.abs(ab - ac - bc) <= accuracy){
        boolean ret = lastFound != null && lastFound.getDirection(l);
        incrementDistance(ac);
        lastFound = new LastFound(last, l);
        Log.println(
            DroidCruise.DEBUG_LEVEL,
            "droidcruise.Route.isInRoute",
            this.toString() + ": <radar>" + l.getLongitude() + ","
                + l.getLatitude() + "</radar>");

        return ret;
      }
      incrementDistance(ab);
    }
    return false;
  }

  @Override
  public int hashCode(){
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj){
    if(this == obj)
      return true;
    if(obj == null)
      return false;
    if(!(obj instanceof Route))
      return false;
    Route other = (Route)obj;
    if(name == null){
      if(other.name != null)
        return false;
    }
    else if(!name.equals(other.name))
      return false;
    return true;
  }

  @Override
  public String toString(){
    return name == null ? null : name;
  }
}
